package com.ctsi.commons.util.collections;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.Serializable;
import java.util.*;

 
 


public class FlexibleMapAccessor<T> implements Serializable {
	private static final Logger  log = LoggerFactory.getLogger(FlexibleMapAccessor.class);
	/**
	 * 
	 */
	private static final long serialVersionUID = 3119375630030153013L;

	protected final String original;

	protected String extName;

	protected final boolean isListReference;

	protected  boolean isAddAtIndex = false;

	protected  boolean isAddAtEnd = false;

	protected  boolean isAscending = true;

	protected int listIndex = -1;

	protected   SubMapAccessor subMapAccessor = null;

	public FlexibleMapAccessor(String name) {
        this.original = name;
        
        // do one quick thing before getting into the Map/List stuff: if it starts with a + or - set the isAscending variable
        if (name != null && name.length() > 0) {
            if (name.charAt(0) == '-') {
                this.isAscending = false;
                name = name.substring(1);
            } else if (name.charAt(0) == '+') {
                this.isAscending = true;
                name = name.substring(1);
            }
        }

        int dotIndex = name.lastIndexOf('.');
        if (dotIndex != -1) {
            this.extName = name.substring(dotIndex+1);
            String subName = name.substring(0, dotIndex);
            this.subMapAccessor = new SubMapAccessor(subName);
        } else {
            this.extName = name;
        }
        int openBrace = this.extName.indexOf('[');
        int closeBrace = (openBrace == -1 ? -1 : this.extName.indexOf(']', openBrace));
        if (openBrace != -1 && closeBrace != -1) {
            String liStr = this.extName.substring(openBrace+1, closeBrace);
            //if brackets are empty, append to list
            if (liStr.length() == 0) {
                this.isAddAtEnd = true;
            } else {
                if (liStr.charAt(0) == '+') {
                    liStr = liStr.substring(1);
                    this.listIndex = Integer.parseInt(liStr);
                    this.isAddAtIndex = true;
                } else {
                    this.listIndex = Integer.parseInt(liStr);
                }
            }
            this.extName = this.extName.substring(0, openBrace);
            this.isListReference = true;
        }else{
        	this.isListReference = false;
        }
	}

	public String getOriginalName() {
		return this.original;
	}

	public boolean getIsAscending() {
		return this.isAscending;
	}

	public boolean isEmpty() {
        return this.original == null || this.original.length() == 0;
	}

	/**
	 * Given the name based information in this accessor, get the value from the
	 * passed in Map. Supports LocalizedMaps by getting a String or Locale
	 * object from the base Map with the key "locale", or by explicit locale
	 * parameter.
	 * 
	 * @param base
	 * @return
	 */
	public T get(Map<String, ? extends Object> base) {
		return get(base, null);
	}

	/**
	 * Given the name based information in this accessor, get the value from the
	 * passed in Map. Supports LocalizedMaps by getting a String or Locale
	 * object from the base Map with the key "locale", or by explicit locale
	 * parameter. Note that the localization functionality is only used when the
	 * lowest level sub-map implements the LocalizedMap interface
	 * 
	 * @param base
	 *            Map to get value from
	 * @param locale
	 *            Optional locale parameter, if null will see if the base Map
	 *            contains a "locale" key
	 * @return
	 */
	public T get(Map<String, ? extends Object> base, Locale locale) {
		if (base == null) {
			return null;
		}

		// so we can keep the passed context
		Map<String, ? extends Object> newBase = null;
		if (this.subMapAccessor != null) {
			try {
				newBase = this.subMapAccessor.getSubMap(base);
			} catch (Exception e) {
				String errMsg = "Error getting map accessor sub-map [" + this.subMapAccessor.extName + "] as part of [" + this.original + "]: " + e.toString();
				log.error(errMsg);
				throw new RuntimeException(errMsg);
			}
		} else {
			newBase = base;
		}

		try {
			T  ret = null;
			if (this.isListReference) {
				List<T> lst = (List<T>) newBase.get(this.extName);
				if (lst == null) {
					return null;
				}
				if (lst.size() == 0) {
					return null;
				}
				ret = lst.get(this.isAddAtEnd ? lst.size() - 1 : this.listIndex);
			} else {
				ret = getByLocale(this.extName, base, newBase, locale);
			}

			// in case the name has a dot like system env values
			if (ret == null) {
				ret = getByLocale(this.original, base, base, locale);
			}

			return ret;
		} catch (Exception e) {
			String errMsg = "Error getting map accessor entry with name [" + this.extName + "] or [" + this.original + "]: " + e.toString();
			log.error(errMsg);
			throw new RuntimeException(errMsg);
		}
	}

	protected T getByLocale(String name, Map<String, ? extends Object> base, Map<String, ? extends Object> sub, Locale locale) {
		if (sub == null) {
			return null;
		}

		T getReturn =(T) sub.get(name);
		return getReturn;

	}

	/**
	 * Given the name based information in this accessor, put the value in the
	 * passed in Map. If the brackets for a list are empty the value will be
	 * appended to the list, otherwise the value will be set in the position of
	 * the number in the brackets. If a "+" (plus sign) is included inside the
	 * square brackets before the index number the value will inserted/added at
	 * that point instead of set at the point.
	 * 
	 * @param base
	 * @param value
	 */
	public void put(Map<String, Object> base, T value) {
		if (base == null) {
			throw new IllegalArgumentException("Cannot put a value in a null base Map");
		}
		if (this.subMapAccessor != null) {
			Map<String, Object> subBase = this.subMapAccessor.getOrCreateSubMap(base);
			if (subBase == null) {
				return;
			}
			base = subBase;
		}
		if (this.isListReference) {
			List<Object> lst = (List<Object>) base.get(this.extName);
			if (lst == null) {
				lst = new ArrayList<Object>();
				base.put(this.extName, lst);
			}
			// if brackets are empty, append to list
			if (this.isAddAtEnd) {
				lst.add(value);
			} else {
				if (this.isAddAtIndex) {
					lst.add(this.listIndex, value);
				} else {
					lst.set(this.listIndex, value);
				}
			}
		} else {
			base.put(this.extName, value);
		}
	}

	/**
	 * Given the name based information in this accessor, remove the value from
	 * the passed in Map. *
	 * 
	 * @param base
	 * @param base
	 *            the Map to remove from
	 * @return the object removed
	 */
	public T  remove(Map<String, ? extends Object> base) {
		if (this.subMapAccessor != null) {
			base = this.subMapAccessor.getSubMap(base);
		}
		if (this.isListReference) {
			List<Object>  lst = (List<Object> ) base.get(this.extName);
			return (T)lst.remove(this.listIndex);
		} else {
			return (T)base.remove(this.extName);
		}
	}
	@Override
	public String toString() {
		return this.original;
	}
    @Override
	public boolean equals(Object that) {
		if (this==that) {
			return true;
		}
		if (!(that instanceof FlexibleMapAccessor)) {
			return false;
		}
		FlexibleMapAccessor<?> thatAcsr = (FlexibleMapAccessor<?>) that;
		if (this.original == null) {
            return thatAcsr.original == null;
		} else {
			return this.original.equals(thatAcsr.original);
		}
	}
    @Override
	public int hashCode() {
		return this.original.hashCode();
	}

	public class SubMapAccessor implements Serializable {
        protected String extName;
        protected boolean isListReference = false;
        protected int listIndex = -1;
        protected SubMapAccessor subMapAccessor = null;

        public SubMapAccessor(String name) {
            int dotIndex = name.lastIndexOf('.');
            if (dotIndex != -1) {
                this.extName = name.substring(dotIndex+1);
                String subName = name.substring(0, dotIndex);
                this.subMapAccessor = new SubMapAccessor(subName);
            } else {
                this.extName = name;
            }
            int openBrace = this.extName.indexOf('[');
            int closeBrace = (openBrace == -1 ? -1 : this.extName.indexOf(']', openBrace));
            if (openBrace != -1 && closeBrace != -1) {
                String liStr = this.extName.substring(openBrace+1, closeBrace);
                this.listIndex = Integer.parseInt(liStr);
                this.extName = this.extName.substring(0, openBrace);
                this.isListReference = true;
            }
        }
        
        public <V> Map<String, V> getSubMap(Map<String, V> base) {
            if (base == null) {
				return null;
			}
            if (this.extName == null) {
				return null;
			}
            if (this.subMapAccessor != null) {
                base = this.subMapAccessor.getSubMap(base);
            }
            if (base == null) {
				return null;
			}
            Object namedObj = base.get(this.extName);
            if (this.isListReference && (namedObj == null || namedObj instanceof List)) {
                List<? extends Object> lst = (List<? extends Object>)(namedObj);
                if (lst == null) {
					return null;
				}
                
                Map<String, V> extMap = null;
                if (lst.size() > this.listIndex) {
                    extMap = ( Map<String, V>)(lst.get(this.listIndex));
                }
                if (extMap == null) {
					return null;
				}
                
                return extMap;
            } else if (namedObj instanceof Map) {
                Map<String, V> extMap = (Map<String, V>)namedObj;
                return extMap;
            } else {
                return null;
            }
        }

        @SuppressWarnings("unchecked")
		public Map<String, Object> getOrCreateSubMap(Map<String, Object> base) {
            if (base == null) {
				return null;
			}
            if (this.extName == null) {
				return null;
			}
            if (this.subMapAccessor != null) {
                base = this.subMapAccessor.getOrCreateSubMap(base);
            }
            Object namedObj = base.get(this.extName);
            if (this.isListReference && (namedObj == null || namedObj instanceof List)) {
                List<Object> lst = (List<Object>)(namedObj);
                if (lst == null) {
                    lst = new ArrayList<Object>();
                    base.put(this.extName, lst);
                }
                
                Map<String, Object> extMap = null;
                if (lst.size() > this.listIndex) {
                    extMap = (Map<String, Object>)(lst.get(this.listIndex));
                }
                if (extMap == null) {
                    extMap = new HashMap<String, Object>();
                    lst.add(this.listIndex, extMap);
                }
                
                return extMap;
            } else if (namedObj == null || namedObj instanceof Map) {
                Map<String, Object> extMap = (Map<String, Object>)(namedObj);
                
                // this code creates a new Map if none is missing, but for a get or remove this is a bad idea...
                if (extMap == null) {
                    extMap = new HashMap<String, Object>();
                    base.put(this.extName, extMap);
                }
                return extMap;
            } else {
                return null;
            }
        }
    }

}
